<?php
/**
 * Abstract Validation Rule
 * All validation rules should extend this class. In order to be capable of
 * being added to a ruleset, they must extend this class.
 * Rules do not need to know what their error messages are. This is by design.
 * They also should be capable of validating one value passed to their
 * isValid() method (rather than an array).
 *
 * @package     Q-Validate
 * @subpackage  Rules
 * @author      Luke Visinoni <luke.visinoni@gmail.com>
 * @copyright   (c) 2010 Luke Visinoni <luke.visinoni@gmail.com>
 * @version     $Id: Abstract.php 37 2010-12-23 02:38:44Z luke.visinoni $
 */
abstract class Q_Rule_Abstract implements Q_Rule_Interface {

    /**
     * @var array The parameters that were passed to this rule
     */
    protected $_params = array();
    
    /**
     * @var array An array of data that the ruleset is validating
     */
    protected $_data = array();
    
    /**
     * @var Q_Rule_Set The ruleset this rule is attached to
     */
    protected $_set;
    
    /**
     * Class Constructor
     * Sets up the params for this rule (for instance, if we are validating that
     * the value is in a particular array, pass the array in here).
     *
     * @param array An array of params the rule uses for configuration
     */
    public function __construct($params = array()) {
    
        $this->setParams($params)
             ->_init();
    
    }
    
    /**
     * An init method to be used by child classes for initialization
     *
     * @return $this
     */
    protected function _init() { return $this; }
    
    /**
     * Set the ruleset this rule is attached to. I am not particularly fond of
     * the method name, but it's kinda necessary.
     *
     * @param Q_Rule_Set The ruleset this rule is attached to
     * @return $this
     */
    public function setSet(Q_Rule_Set $set) {
    
        $this->_set = $set;
        return $this;
    
    }
    
    /**
     * Retreive the set this rule is a part of. This will return null if the
     * rule is not part of a set.
     *
     * @return Q_Rule_Set|null If this rule is part of a set, the set will be
     * returned, otherwise it will return null.
     * @todo This is a terrible method name, but I'm not sure there is anything
     * that can be done about that.
     */
    public function getSet() {
    
        return $this->_set;
    
    }
    
    /**
     * Set a single parameter
     *
     * @param string $key The key which is used to retrieve the parameter
     * @param mixed $val The value of the parameter
     * @return $this
     */
    public function setParam($key, $val) {
    
        $this->_params[$key] = $val;
        return $this;
    
    }
    
    /**
     * Set multiple params
     *
     * @param array $params An associative array of parameters
     * @return $this
     */
    public function setParams($params) {
    
        foreach ($params as $key => $param) {
            $this->_params[$key] = $param;
        }
        return $this;
    
    }
    
    /**
     * Get a parameter by key
     *
     * @param string|false $key The key for the parameter being retrieved. If
     * there is no parameter associated with this key, false is returned
     */
    public function getParam($key) {
    
        if (array_key_exists($key, $this->_params)) return $this->_params[$key];
        return false;
    
    }
    
    /**
     * Validate data set
     * I'm sort of confused why this method is here, but apparently it is
     * necessary. Apparently it makes fields like field[name] capable of
     * being validated.
     *
     * @param array $data The data array being validated
     * @return boolean Whether or not the data array is valid
     * @todo Find out if this method is even being used and clean it up
     * @todo This needs to support field names like database[user] and mailserver[host]
     */
    public function validate($data) {
    
        if (!is_array($data)) {
            if ($this->isOptional() && empty($data)) return true;
            return $this->isValid($data);
        }
        $this->_data = $data;
        $field = $this->getParam('field');
        // the reason I don't have to check that the first part === false is
        // because I want at least one char before it
        if (strpos($field, '[') && strpos($field, ']')) {
            // multi-dimensional, find the value based on the field's array index(es)
            // foo[bar][baz]
            $pos = strpos($field, '[');
            $base = substr($field, 0, $pos);
            $index = substr($field, $pos);
            $indexes = array($base);
            while (preg_match('/\[([^\]]+)\]/', $index, $match)) {
                $indexes[] = $match[1];
                $index = substr($index, strlen($match[0]));
            }
            $array = $this->_data;
            foreach ($indexes as $i) {
                if (array_key_exists($i, $array)) {
                    $array = $array[$i];
                } else {
                    $array = $array;
                }
            }
            if (!is_array($array)) $value = $array;
            else $value = $this->getValueByField();
            if (empty($value)) {
                if ($this->isOptional()) return true;
            }
            return $this->isValid($value);
        } else {
            $value = $this->getValueByField();
            if ($this->isOptional() && empty($value)) return true;
            return $this->isValid($this->getValueByField());
        }
        return false;
    
    }
    
    /**
     * Retrieve a value in the data array or, if not available, a default value
     * If the field name is not specified, this will attempt to return the field
     * which this rule was attached to.
     * 
     * @param string $field The field you want to retrieve
     * @param mixed $default The default value to return if field is unavailable
     * @return mixed Either the specified field or the passed-in default value
     */
    protected function getValueByField($field = null, $default = null) {
    
        if (is_null($field)) $field = $this->getParam('field');
        return (array_key_exists($field, $this->_data)) ? $this->_data[$field] : $default;
    
    }
    
    /**
     * Determine whether this rule is optional
     * Rules may be specified "optional" in which case, they only need to pass
     * validation if they are in the data set.
     *
     * @return boolean Whether this rule is optional
     */
    public function isOptional() {
    
        return ($this->getParam('optional') && $this->getParam('optional') == true);
    
    }
    
    /**
     * Set the field this rule applies to
     *
     * @param string $field The field for which this rule applies
     * @return $this
     */
    public function setField($field) {
    
        $this->_params['field'] = (string) $field;
        return $this;
    
    }
    
    /**
     * When specifying error messages, it is sometimes convenient to have access
     * to the parameter values that were passed in. This replaces {:param} with
     * the parameter value. For instance, if we are creating a rule for min and
     * max length, we can create a message like "This value must be between
     * {:min} and {:max} characters".
     *
     * @param string $message The message to do the replacements on
     * @return string The message with replacements made
     */
    public function doMessageReplacement($message) {
    
        foreach ($this->_params as $key => $val) {
            if (is_string($val) || is_int($val) || (is_object($val) && method_exists($val, '__toString'))) {
                $token = '{:' . $key . '}';
                $val = (string) $val;
                $message = str_replace($token, $val, $message);
            }
        }
        return ucfirst($message);
    
    }
    
    /**
     * Validate a regex pattern
     * A lot of rules need to use regex to do their validation. This method is
     * just a convenience method for doing these pattern matches.
     *
     * @param string $pattern The regex pattern to match
     * @param string $value The value to match against
     * @return boolean Whether the pattern matched or not
     */
    protected function _validatePattern($pattern, $value) {
    
        return (boolean) preg_match($pattern, $value);
    
    }
    
    /**
     * This is the method used to determine whether the rule passed or not
     * Every rule must extend this method to do the validation.
     *
     * @param mixed $value The value to validate
     * @return boolean Whether or not the rule passed
     */
    abstract public function isValid($value);

}
